<?php

set_time_limit(0);

/**
 * Write a row in the log file. If the log file not exists it will be create.
 * @param $text The sentence to be logged.
 */
function write_log($text) {
	$var=fopen(datalist_get("dataroot") . "mtcd.log", "a+");
	fwrite($var, "[" . date("Y/m/d H:i:s",time()) . "] ");
	fwrite($var, $text);
	fwrite($var, "\r\n");
	fclose($var);
}

/**
 * Get a list of synonyms of a word.
 * @param $word The word to get synonyms.
 * @return List of word synonyms.
 */
function get_synonyms($word) {

	$plugin = elgg_get_plugin_from_id("mtcd");
	$path_wn = $plugin->getAllSettings();

	exec($path_wn["path_wn"] . " " . $word . " -synsn",$names);
	exec($path_wn["path_wn"] . " " . $word . " -synsv",$verbs);

	$result = array_merge($names,$verbs);

	$synonyms = array(); // list of synonyms
	$interesting_zone = false;

	foreach($result as $row) {
		if($interesting_zone) {
			$synonyms = array_merge($synonyms,explode(", ", $row));
			$interesting_zone = false;
		}

		if(substr($row,0,5) == "Sense") {
			$interesting_zone = true;
		}
	}

	return array_unique($synonyms);
}

/**
 * Get a list of hypernyms of a word.
 * @param $word The word to get hypernyms.
 * @return List of word hypernyms.
 */
function get_hypernyms($word) {

	$plugin = elgg_get_plugin_from_id("mtcd");
	$path_wn = $plugin->getAllSettings();

	exec($path_wn["path_wn"] . " " . $word . " -hypen",$names);
	exec($path_wn["path_wn"] . " " . $word . " -hypev",$verbs);

	$result = array_merge($names,$verbs);

	$hypernyms = array(); // list of hypernyms
	$interesting_zone = false;

	foreach($result as $row) {
		$normalizeRow = trim($row);
		if(substr($normalizeRow,0,3) == "=> ") {
			$normalizeRow = substr($normalizeRow,3);
			$interesting_zone = true;
		}

		if(substr($normalizeRow,0,14) == "INSTANCE OF=> ") {
			$normalizeRow = substr($normalizeRow,14);
			$interesting_zone = true;
		}

		if($interesting_zone) {
			$hypernyms = array_merge($hypernyms,explode(", ", $normalizeRow));
			$interesting_zone = false;
		}
	}

	return array_unique($hypernyms);
}

/**
 * Create a dictionary of words associated with other words which are related in some way.
 * @param $metaname List of descriptors names between tags, interests or both.
 * @param $type Type of dictionary between synonyms and hypernyms.
 * @return Dictionary of all synonyms or hypernyms found between descriptors.
 */
function get_dictionary($metaname, $type) {

	$count_metadata = elgg_get_metadata(array("count"=> TRUE, "metadata_names" => $metaname, "limit" => 0));

	$metadata = elgg_get_tags(array("tag_names"=> $metaname, "limit"=> $count_metadata ) );
	$dictionary = array();

	foreach ($metadata as $meta) {
		$lowerMeta = strtolower($meta->tag);
		$dictionary[$lowerMeta] = array();
		$relatedWords = array();

		switch ($type) {
			case "hypernyms":
				$relatedWords = get_hypernyms($meta->tag);
				break;
					
			default:
				$relatedWords = get_synonyms($meta->tag);
			break;
		}
		foreach ($relatedWords as $relatedWord) {
			foreach ($metadata as $otherMeta) {
				if($otherMeta->tag == $meta->tag) {
					continue;
				}
				if(strtolower($relatedWord) == strtolower($otherMeta->tag)) {
					array_push($dictionary[$lowerMeta],strtolower($otherMeta->tag));
				}
			}
		}

		if (empty($dictionary[$lowerMeta])) {
			unset($dictionary[$lowerMeta]);
		}
	}

	foreach (array_keys($dictionary) as $key) {
		write_log("Correspondence between " . $key. " and " . implode(", ", $dictionary[$key]) ." found");
	}

	return $dictionary;
}

/**
 * Get and filter data from Elgg database to produce a dataset for the clustering procedure.
 * @param $synonyms Dictionary of all synonyms found between descriptors.
 * @param $hypernyms Dictionary of all hypernyms found between descriptors.
 * @param $type Type of entity on which build dataset.
 * @param $subtypes Subtypes of entity on which build dataset.
 */
function generate_dataset($synonyms, $hypernyms, $type, $subtypes = ELGG_ENTITIES_NO_VALUE) {

	$i=1;					// placeholder
	$attributes = array();	// list of descriptors
	$data = array();		// entity-descriptor matrix

	// get all entities of a given type
	foreach (elgg_get_entities(array("type"=>$type, "subtypes"=>$subtypes, "limit"=>0)) as $entity) {
		$entityDescriptors = array();

		// get all entity descriptors
		foreach ($entity->getMetaData(($type == "object") ? "tags" : "interests") as $descriptor) {
			$lowerDescriptor = strtolower($descriptor);
			$entityDescriptors = array_merge($entityDescriptors,array($lowerDescriptor));

			if(array_key_exists($lowerDescriptor,$synonyms)) {
				$entityDescriptors = array_merge($entityDescriptors,$synonyms[$lowerDescriptor]);
			}

			if(array_key_exists($lowerDescriptor,$hypernyms)) {
				$entityDescriptors = array_merge($entityDescriptors,$hypernyms[$lowerDescriptor]);
			}
		}
		$entityDescriptors = array_unique($entityDescriptors);

		// save new descriptor
		foreach (array_diff($entityDescriptors,array_keys($attributes)) as $descriptor) {
			$attributes[$descriptor] = $i;
			$i++;
		}

		// save descriptor placeholders
		$data[$entity->guid] = array();
		foreach ($entityDescriptors as $descriptor) {
			array_push($data[$entity->guid],$attributes[$descriptor]);
		}
		sort($data[$entity->guid],SORT_NUMERIC);
	}

	write_arff(($type == "object") ? $subtypes[0] : $type, $attributes, $data);
}

/**
 * Formatting a dataset and write it in an ARFF file. If the file not exists it will be create.
 * @param $relation Dataset relation.
 * @param $attributes List of dataset attributes.
 * @param $data List of dataset data.
 */
function write_arff($relation, $attributes, $data) {

	$var=fopen(datalist_get("dataroot") . $relation . ".arff","w+");
	fwrite($var, "@relation $relation \r\n");

	fwrite($var, "@attribute entity { " . implode(",", array_keys($data)) . " }\r\n");

	foreach (array_keys($attributes) as $tag) {
		fwrite($var, "@attribute $tag { 0, 1 }\r\n");
	}
	fwrite($var, "@data\r\n");

	foreach (array_keys($data) as $guid) {
		if(empty($data[$guid])){
			fwrite($var, "{ 0 $guid }\r\n");
		}
		else {
			fwrite($var, "{ 0 $guid, " . implode(" 1, ", $data[$guid]) . " 1 }\r\n");
		}
	}

	fclose($var);
}

/**
 * Use simple k-means to obtain omogeneous clusters of entities.
 * @param $relation Dataset relation to be clustered.
 * @param $number_of_cluster Number of cluster that algorithm have to return.
 * @return List of cluster. A cluster is a list of entity GIUDs.
 */
function get_clusters($relation,$number_of_cluster)
{
	$plugin = elgg_get_plugin_from_id("mtcd");
	$settings = $plugin->getAllSettings();

	exec("java -cp " . $settings["path_weka"] . " weka.clusterers.SimpleKMeans -t ". datalist_get("dataroot") . $relation . ".arff -c first -N $number_of_cluster -A \"weka.core.EuclideanDistance -R first-last\" -I 500 -S 10", $result);

	$interesting_zone = false;
	$clusters = array();

	for($i=0; $i<count($result); $i++)
	{
		if($interesting_zone) {
			if($result[$i] == "") {
				$interesting_zone = false;
			}
			else {
				$class_entity = explode(" |",$result[$i]);
				$classes = explode(" ",trim($class_entity[0]));
				$class = array_search("1", $classes);
				if(isset($clusters[$class])) {
					array_push($clusters[$class],$class_entity[1]);
				}
				else {
					$clusters[$class] = array($class_entity[1]);
				}
			}
		}

		if ($result[$i] == "Classes to Clusters:") {
			$interesting_zone = true;
			$i += 2;
		}
	}

	foreach ($clusters as $cluster) {
		write_log("A cluster with " . count($cluster) . " element found for $relation");
	}

	return $clusters;
}

/**
 * Delete all clusters of a given type.
 * @param $type Type of cluster to be deleted.
 */
function delete_clusters($type) {
	foreach (elgg_get_entities(array("type"=>"object", "subtype"=>"cluster", "limit"=>0)) as $entity) {
		if(substr($entity->title, strlen($type) * -1) == $type) {
			$entity->delete();
		}
	}
}

/**
 * Save all clusters and create the relationships between the clusters and the contained entities.
 * @param $clusters List of clusters. A cluster is a list of entity GIUDs.
 * @param $type The type of cluster.
 */
function save_clusters($clusters,$type)
{
	delete_clusters($type);
	$i = 1;

	foreach ($clusters as $cluster) {

		//create cluster
		$elgg_obj = new ElggObject();
		$elgg_obj->title = "cluster" . ($i)  ."_" . $type;
		$elgg_obj->subtype = "cluster";
		$elgg_obj->access_id = 2; //setting the object to become public
		$elgg_obj->save();

		//create relatioship
		foreach ($cluster as $relationship) {
			$elgg_obj->addRelationship($relationship,$type . "_element");
		}
		$i++;
	}
}

/**
 * Iteratively performs the clustering process by changing the K value until the clusters cardinality isn't less than the maximum allowed.
 * @param $type Type of entity on which performs the clustering process.
 * @param $subtype Subtype of entity on which performs the clustering process.
 */
function init_clustering($type, $subtype = ELGG_ENTITIES_NO_VALUE) {
	if($type == object) {
	 	$relation = $subtype;
	}
	else {
		$relation = $type;
	}
	
	write_log("Start clustering for $relation");
	
	$clusterCardinality = 25;
	$count = elgg_get_entities(array("count"=> TRUE, "type" => $type, "subtype" => $subtype, "limit" => 0));
	$k = round($count/$clusterCardinality);
	if($k<2){
		$k = 2;
	}
	
	$invalidCluster = true;
	while ($invalidCluster) {
		$invalidCluster = false;
		$clusters = get_clusters($relation, $k);
		foreach ($clusters as $cluster) {
			if(count($cluster) > $clusterCardinality) {
				$invalidCluster = true;
				$k++;
				break;
			}
		}
	}
	write_log("The K value choosen is $k");
	save_clusters($clusters, $relation);
	
	write_log("End clustering for $relation");
}

/**
 * Clustering procedure. Called by Cron once a week.
 * @param $hook Used by cron, how set by elgg core developer.
 * @param $entity_type Used by cron, how set by elgg core developer.
 * @param $returnvalue Used by cron, how set by elgg core developer.
 * @param $params Used by cron, how set by elgg core developer.
 */
function clustering($hook, $entity_type, $returnvalue, $params) {

	write_log("Createing dictionary");
	$synonyms = get_dictionary(array("tags","interests"),"synonyms");
	$hypernyms = get_dictionary(array("tags","interests"),"hypernyms");
	write_log("Dictionary created");

	$plugin = elgg_get_plugin_from_id('mtcd');
	$settings = $plugin->getAllSettings();

	if(isset($settings['recommender_user'] ) && $settings['recommender_user'] == 'true') {
		generate_dataset($synonyms, $hypernyms, "user");
		init_clustering("user");
	}
	if(isset($settings['recommender_group'] ) && $settings['recommender_group'] == 'true') {
		generate_dataset($synonyms, $hypernyms, "group");
		init_clustering("group");
	}
	if(isset($settings['recommender_blog'] ) && $settings['recommender_blog'] == 'true') {
		generate_dataset($synonyms, $hypernyms, "object", array("blog"));
		init_clustering("blog");
	}
	if(isset($settings['recommender_file'] ) && $settings['recommender_file'] == 'true') {
		generate_dataset($synonyms, $hypernyms, "object", array("file"));
		init_clustering("file");
	}
	if(isset($settings['recommender_bookmarks'] ) && $settings['recommender_bookmarks'] == 'true') {
		generate_dataset($synonyms, $hypernyms, "object", array("bookmarks"));
		init_clustering("bookmarks");
	}
	if(isset($settings['recommender_page'] ) && $settings['recommender_page'] == 'true') {
		generate_dataset($synonyms, $hypernyms, "object", array("page","top_page"));
		init_clustering("page");
	}
}

/**
 * Inizialize the plugin. Called when the system is initialized.
 */
function recommender_init() {
	elgg_register_widget_type("recommender", "Recommender System", "Knowledge-based recommender system");
	elgg_register_css("style.css", $vars["url"] . "mod/mtcd/views/default/widgets/recommender/style.css");
	elgg_register_plugin_hook_handler("cron", "weekly", "clustering");
}

/**
 * Remove all clusters and the subtype cluster. Called when the plugin is deactiveted.
 */
function deactivation(){
	delete_clusters("user");
	delete_clusters("group");
	delete_clusters("file");
	delete_clusters("blog");
	delete_clusters("bookmarks");
	delete_clusters("page");
	remove_subtype("object","cluster");
}

elgg_register_event_handler("init", "system", "recommender_init");
elgg_register_event_handler("deactivate","plugin","deactivation");
elgg_register_action("mtcd/settings/save", $CONFIG->pluginspath . "mtcd/actions/save.php", "admin");

?>